bottlepy 一叶知秋

在工作的微服务实践中,发现同事写的一个 proxy 用了 bottle 这个框架,感觉挺有意思,于是稍稍研究了一下。

这篇文章会总结一下 bottle,但不会讲解 bottle 的使用方法。

为什么要搞定 bottlepy

因为搞定一个 Web 框架会对 Web 开发有更深刻的理解。

bottle 是一个高效、简洁、轻量的 web 框架,整个框架只有一个 bottle.py 文件,最新版本 0.12.10 只有 3751行。只有一个文件,没有其他依赖,这一点就已经很酷了。它和 Flask 很像,同样是用装饰器来表现路由,同样都是微框架,那么搞定 bottle.py 有意义吗,直接看 Flask 源代码不好吗?当然有意义,Flask 基于 Werkzeug(WSGI 工具集) 和 jinja2(模板引擎), 代码量不多,很优雅,扩展性也很强,但和单文件的 bottle.py 相比就显得厚重了。从 bootle.py 的源代码中,我们可以学到的是 Web 框架的核心是什么,掌握了核心思想,再去处理一些 Web 开发过程中的一些边边角角的问题就只需要利用经验了。

Web 应用框架

首先我们应该对 Web 应用框架有一个简单的了解。在我看来,基本所有的 Web 应用框架都做了一件最基本的事情:接受 HTTP 请求,(这个请求可能是 GET,POST,PUT等等)接收到请求后,Server 端进行一些处理,返回给客户端一个回应,就这么简单。一般来说,Web 应用框架,还会将一些通用的功能集成在一起,比如数据库访问接口、模板、会话管理,异常处理等等,提高代码的复用性,减轻 Web 开发时程序员的工作负荷。

bottlepy 概述

『Bottle 是一个快速,简单,轻量级的 Python WSGI Web 框架。单一文件,只依赖 Python 标准库』

功能特点(翻译自官网):

  • URL 映射(Routing): 将 URL 请求映射到 Python 函数,支持动态 URL,且 URL 更简洁。
  • 模板(Template): 快速且 pythonic 的内置模板引擎,同时支持 makojinja2cheetah
  • 基础功能(Utilities): 方便地访问表单数据,上传文件,cookie,HTTP header 以及其他的元数据
  • 服务器(Server): 内置了开发服务器,且支持 paste, fapws3, bjoern, gae, cherrypy 等符合 WSGI 标准的 HTTP 服务器。

Bottle 的功能是很基础的,单文件,很酷,也有一些让人讨厌的地方:

  • 代码风格不规范,对于有代码洁癖的我来说简直是折磨
  • request,response 都是全局变量
  • 完全使用标准库的内容,没有使用 six 这样的包来解决 python2,python3的兼容性

bottlepy 源码解析

在看 bottle.py 的代码之前,我们应该先对 wsgi 以及基于 python 的 web 应用的典型流程有所了解。

wsgi 全称 Web Server Gateway Interface,也就是 Web 服务器网关接口,它实际上是一种规范,定义了 Python 世界中,Web 服务器与 Web 应用程序之间的接口,这个规范即 pep-3333

基于 bottle 的 web 应用的典型流程是这样的:

  1. wsgi 启动后,加载 bottle 应用,监听某个端口
  2. 请求进来之后,会调用 WSGI 应用程序的 __call__ 方法,__call__ 方法能接收到 environ, start_response 参数,这些都是 wsgi 协议中定义好的。这时 Bottle 开始作为 Web 应用开始处理请求了
  3. bottle 会根据传过来的 environ 字典,初始化 request,response 对象,根据 url 的值,匹配代码中装饰器生成的路由器对象,找到相应的处理函数
  4. 调用处理函数,根据调用的参数返回结果
  5. 结果被封装为 wsgi 规范规定的格式,通过 start_response 返回给 wsgi 服务器。这时请求得到回应,即完成了一次 HTTP 协议的请求的响应过程。

wsgi server

首先要知道的是,每写一个 bottle 应用,我们都需要一个 bottle.py 中定义的 app,即有一个初始化的过程,在 bottle.py 中,这个入口在最后几行。

1
2
app = default_app = AppStack()
app.push()

而 AppStack 会用栈的形式保存所有 Application,一个 Application 就是一个 Bottle 实例。

1
2
3
4
5
6
7
8
9
class AppStack(list):
def __call__(self):
return self[-1]

def push(self, value=None):
if not isinstance(value, Bottle):
value = Bottle()
self.append(value)
return value

Bottle 类中跟 wsgi 规范相关的代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Bottle(object):
def __init__(self, catchall=True, autojson=True):
# balabala
pass
...
def wsgi(self, environ, start_response):
try:
out = self._cast(self._handle(environ))
if response._status_code in (100, 101, 204, 304)\
or environ['REQUEST_METHOD'] == 'HEAD':
if hasattr(out, 'close'): out.close()
out = []
start_response(response._status_line, response.headerlist)
return out
except (KeyboardInterrupt, SystemExit, MemoryError):
raise
except Exception:
if not self.catchall: raise
err = '<h1>Critical error while processing request: %s</h1>' \
% html_escape(environ.get('PATH_INFO', '/'))
if DEBUG:
err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
'<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
% (html_escape(repr(_e())), html_escape(format_exc()))
environ['wsgi.errors'].write(err)
headers = [('Content-Type', 'text/html; charset=UTF-8')]
start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info())
return [tob(err)]

def __call__(self, environ, start_response):
return self.wsgi(environ, start_response)

WSGI 规范中提到,Web 框架必须向 WSGI Server 提供 application 对象,用于处理用户请求,在 Bottle 中,application 对象就是一个 Bottle 实例,而且它是一个 callable 对象,Bottle__call__ 直接调用 wsgi 方法,wsgi() 方法会生成输出,调用 start_response() 回调函数通过 WSGI Server 响应请求,而请求的处理逻辑在 _handle() 方法中,我们接着看 _handle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def _handle(self, environ):
path = environ['bottle.raw_path'] = environ['PATH_INFO']
if py3k:
try:
environ['PATH_INFO'] = path.encode('latin1').decode('utf8')
except UnicodeError:
return HTTPError(400, 'Invalid path string. Expected UTF-8')

try:
environ['bottle.app'] = self
request.bind(environ)
response.bind()
try:
self.trigger_hook('before_request')
route, args = self.router.match(environ)
environ['route.handle'] = route
environ['bottle.route'] = route
environ['route.url_args'] = args
return route.call(**args)
finally:
self.trigger_hook('after_request')

except HTTPResponse:
return _e()
except RouteReset:
route.reset()
return self._handle(environ)
except (KeyboardInterrupt, SystemExit, MemoryError):
raise
except Exception:
if not self.catchall: raise
stacktrace = format_exc()
environ['wsgi.errors'].write(stacktrace)
return HTTPError(500, "Internal Server Error", _e(), stacktrace)

可以看到,_handle 其实就做了两件事,1. 根据 environ 的值匹配路由 2. 调用路由对应的处理函数,返回处理结果,匹配路由进行处理的过程我们在下文会继续深入。回到 out = self._cast(self._handle(environ)) 这一行,我们继续看 def _cast(self, out, peek=None)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def _cast(self, out, peek=None):
if not out:
if 'Content-Length' not in response:
response['Content-Length'] = 0
return []
if isinstance(out, (tuple, list))\
and isinstance(out[0], (bytes, unicode)):
out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
if isinstance(out, unicode):
out = out.encode(response.charset)
if isinstance(out, bytes):
if 'Content-Length' not in response:
response['Content-Length'] = len(out)
return [out]

if isinstance(out, HTTPError):
out.apply(response)
out = self.error_handler.get(out.status_code, self.default_error_handler)(out)
return self._cast(out)
if isinstance(out, HTTPResponse):
out.apply(response)
return self._cast(out.body)

if hasattr(out, 'read'):
if 'wsgi.file_wrapper' in request.environ:
return request.environ['wsgi.file_wrapper'](out)
elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
return WSGIFileWrapper(out)

try:
iout = iter(out)
first = next(iout)
while not first:
first = next(iout)
except StopIteration:
return self._cast('')
except HTTPResponse:
first = _e()
except (KeyboardInterrupt, SystemExit, MemoryError):
raise
except Exception:
if not self.catchall: raise
first = HTTPError(500, 'Unhandled exception', _e(), format_exc())

if isinstance(first, HTTPResponse):
return self._cast(first)
elif isinstance(first, bytes):
new_iter = itertools.chain([first], iout)
elif isinstance(first, unicode):
encoder = lambda x: x.encode(response.charset)
new_iter = imap(encoder, itertools.chain([first], iout))
else:
msg = 'Unsupported response type: %s' % type(first)
return self._cast(HTTPError(500, msg))
if hasattr(out, 'close'):
new_iter = _closeiter(new_iter, out.close)
return new_iter

_cast() 对结果进行处理,转换为符合 WSGI 标准的格式,最终返回给 wsgi server。

路由

Bottle 中与路由相关的属性是 routesrouter

routes 是一个列表,存放该应用所有的 Route 对象,而 router 是一个 Router 实例,它将请求映射到对应的 Route 对象,Route 对象会调用对应的处理函数处理请求。

记得 bottle 官方文档 中给出的例子是这样的:

1
2
3
4
5
6
7
from bottle import route, run, template

@route('/hello/<name>')
def index(name):
return template('<b>Hello {{name}}</b>!', name=name)

run(host='localhost', port=8080)

我们先来看看这个使用装饰器路由的功能是怎么实现的。

添加路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def route(self, path=None, method='GET', callback=None, name=None,
apply=None, skip=None, **config):
if callable(path): path, callback = None, path
plugins = makelist(apply)
skiplist = makelist(skip)
def decorator(callback):
if isinstance(callback, basestring): callback = load(callback)
for rule in makelist(path) or yieldroutes(callback):
for verb in makelist(method):
verb = verb.upper()
route = Route(self, rule, verb, callback, name=name,
plugins=plugins, skiplist=skiplist, **config)
self.add_route(route)
return callback
return decorator(callback) if callback else decorator

route() 方法是一个带参数的装饰器,使用 @route() 装饰器时,它会根据 rule, verb, callback 等变量实例化一个 Route 对象,并调用 add_route

1
2
3
4
def add_route(self, route):
self.routes.append(route)
self.router.add(route.rule, route.method, route, name=route.name)
if DEBUG: route.prepare()

add_route 会将该 Route 实例加入到 self.routes 列表,同时在 self.router 注册该对象。注册路由会构成一个『路径->处理方法』的匹配,我们深入的 Router 的 add 方法看看这个路由注册是怎么实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def add(self, rule, method, target, name=None):
anons = 0
keys = []
pattern = ''
filters = []
builder = []
is_static = True

for key, mode, conf in self._itertokens(rule):
if mode:
is_static = False
if mode == 'default': mode = self.default_filter
mask, in_filter, out_filter = self.filters[mode](conf)
if not key:
pattern += '(?:%s)' % mask
key = 'anon%d' % anons
anons += 1
else:
pattern += '(?P<%s>%s)' % (key, mask)
keys.append(key)
if in_filter: filters.append((key, in_filter))
builder.append((key, out_filter or str))
elif key:
pattern += re.escape(key)
builder.append((None, key))

self.builder[rule] = builder
if name: self.builder[name] = builder

if is_static and not self.strict_order:
self.static.setdefault(method, {})
self.static[method][self.build(rule)] = (target, None)
return

try:
re_pattern = re.compile('^(%s)$' % pattern)
re_match = re_pattern.match
except re.error:
raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e()))

if filters:
def getargs(path):
url_args = re_match(path).groupdict()
for name, wildcard_filter in filters:
try:
url_args[name] = wildcard_filter(url_args[name])
except ValueError:
raise HTTPError(400, 'Path has wrong format.')
return url_args
elif re_pattern.groupindex:
def getargs(path):
return re_match(path).groupdict()
else:
getargs = None

flatpat = _re_flatten(pattern)
whole_rule = (rule, flatpat, target, getargs)

if (flatpat, method) in self._groups:
if DEBUG:
msg = 'Route <%s %s> overwrites a previously defined route'
warnings.warn(msg % (method, rule), RuntimeWarning)
self.dyna_routes[method][self._groups[flatpat, method]] = whole_rule
else:
self.dyna_routes.setdefault(method, []).append(whole_rule)
self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1

self._compile(method)

add 方法可以增加或替换一个已存在的路由。同一个视图函数,可以使用多个路由装饰器来装饰,这里首先会遍历某个路由装饰器指定的路由规则,确定是静态路由还是动态路由,如果是静态路由,添加到 static 字典里就 return 了。如果是动态路由,其实处理规则也类似,只是需要先编译正则表达式再加入 dyna_routes 字典里

匹配路由

路由已经构建好了,接下来我们看看 http 请求进来时,路由是怎么匹配的。还记得 _handle 方法中,处理 request 请求时有这么一行:route, args = self.router.match(environ),我们看看这个方法是怎么实现路由匹配的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def match(self, environ):
verb = environ['REQUEST_METHOD'].upper()
path = environ['PATH_INFO'] or '/'
target = None
if verb == 'HEAD':
methods = ['PROXY', verb, 'GET', 'ANY']
else:
methods = ['PROXY', verb, 'ANY']

for method in methods:
if method in self.static and path in self.static[method]:
target, getargs = self.static[method][path]
return target, getargs(path) if getargs else {}
elif method in self.dyna_regexes:
for combined, rules in self.dyna_regexes[method]:
match = combined(path)
if match:
target, getargs = rules[match.lastindex - 1]
return target, getargs(path) if getargs else {}

allowed = set([])
nocheck = set(methods)
for method in set(self.static) - nocheck:
if path in self.static[method]:
allowed.add(verb)
for method in set(self.dyna_regexes) - allowed - nocheck:
for combined, rules in self.dyna_regexes[method]:
match = combined(path)
if match:
allowed.add(method)
if allowed:
allow_header = ",".join(sorted(allowed))
raise HTTPError(405, "Method not allowed.", Allow=allow_header)

raise HTTPError(404, "Not found: " + repr(path))

首先从 environ 变量中拿到 http 方法和请求的路径,首先匹配注册的静态路由,匹配不成功,接着匹配动态路由,调用 dyna_regexes 中已经编译好的正则表达式进行匹配。如果匹配上了,返回该路径对应的路由以及相应的参数,如果还是没有匹配上,匹配相同路径的其他方法,如果匹配上了,返回 405 Method not allowed 错误,否则返回 404 Not found 错误。成功匹配后,通过 route.call(**args) 进行调用。

http 协议

Bottle 对每一次请求都会把参数保存在当前线程中,通过继承 threading.local 实现线程安全。请求和相应分别存储在全局变量 requestresponse 中,这样一来,我们就可以用类似 request.headers.get('Authorization') 这样的代码获取 request 对象中的一些信息。在 bottle.py 中有:

1
2
request = LocalRequest()
response = LocalResponse()

他们是线程安全的,当前的请求总是在当前的 request/response 变量中。

request

深入到 LocalRequest 中。

1
2
3
class LocalRequest(BaseRequest):
bind = BaseRequest.__init__
environ = local_property()

可以看到,LocalRequest 实际上继承于 BaseReuestBaseRequest 类的内容很多,这里我们只捡典型的来看。

1
2
3
def __init__(self, environ=None):
self.environ = {} if environ is None else environ
self.environ['bottle.request'] = self

首先看 __init__ 方法,BaseRequest 对 environ 进行了封装,在其他方法中可以利用 self.environ 直接访问。

在 request 中有很多属性,比如我们可以通过 request.body 来得到 http 请求中的 body。其中大部分为一般属性,用 Python 的 @property 装饰器定义,还有部分属性使用 bottle.py 自定义的 DictProperty 来定义,这样可以设置只读属性,还可以使用 attr,key 两级的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DictProperty(object):
def __init__(self, attr, key=None, read_only=False):
self.attr, self.key, self.read_only = attr, key, read_only

def __call__(self, func):
functools.update_wrapper(self, func, updated=[])
self.getter, self.key = func, self.key or func.__name__
return self

def __get__(self, obj, cls):
if obj is None: return self
key, storage = self.key, getattr(obj, self.attr)
if key not in storage: storage[key] = self.getter(obj)
return storage[key]

def __set__(self, obj, value):
if self.read_only: raise AttributeError("Read-Only property.")
getattr(obj, self.attr)[self.key] = value

def __delete__(self, obj):
if self.read_only: raise AttributeError("Read-Only property.")
del getattr(obj, self.attr)[self.key]

当我们使用 @DictProperty('environ', 'bottle.request.headers', read_only=True) 这样的装饰器时,被装饰的函数的返回值实际在 self.environ['bottle.request.headers'] 中,而且可以确保这个属性只是可读的。
还记得 LocalRequest 中的 environ = local_property() 这行,它确保了 environ 是线程安全的。

1
2
3
4
5
6
7
8
9
10
def local_property(name=None):
if name: depr('local_property() is deprecated and will be removed.') #0.12
ls = threading.local()
def fget(self):
try: return ls.var
except AttributeError:
raise RuntimeError("Request context not initialized.")
def fset(self, value): ls.var = value
def fdel(self): del ls.var
return property(fget, fset, fdel, 'Thread-local property')

response

response 和 request 非常类似,不过没有只可读的属性,因为 response 的使用场景是用户设置如 cookieheaderbody 之类的返回的值,它将通过 WSGI 返回给发送请求的用户。

trigger_hook 钩子函数

_handle 方法中,我们记得有这么一行:self.trigger_hook('before_request'),实际上在 Bottle 中,我们可以在对 request 处理前和处理后添加钩子函数,这样的话在 request 的处理前后可以自动调用这些函数,目前支持三种钩子函数,before_request, after_request, app_reset,比如在 request 处理前修改 REQUEST_METHOD 这样的场景就可以使用 before_request 函数。

增加一个 hook,可以通过调用 add_hook,或使用 hook 装饰器:

1
2
3
4
5
6
7
8
9
10
11
def add_hook(self, name, func):
if name in self.__hook_reversed:
self._hooks[name].insert(0, func)
else:
self._hooks[name].append(func)

def hook(self, name):
def decorator(func):
self.add_hook(name, func)
return func
return decorator

当需要 trigger 时,调用 trigger_hook:

1
2
def trigger_hook(self, __name, *args, **kwargs):
return [hook(*args, **kwargs) for hook in self._hooks[__name][:]]

模板引擎

Bottle 内置了一个称为 SimpleTemplate 的模板引擎,利用 StplParser 进行模板渲染。同时通过 adapter 的方式支持 makocheetahjinja2 模板渲染。通过 from bottle import template 导入模板函数后,利用 template('<b>Hello </b>!', name=name) 这种方式传入相应调用即可得到渲染后的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def template(*args, **kwargs):
tpl = args[0] if args else None
adapter = kwargs.pop('template_adapter', SimpleTemplate)
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
tplid = (id(lookup), tpl)
if tplid not in TEMPLATES or DEBUG:
settings = kwargs.pop('template_settings', {})
if isinstance(tpl, adapter):
TEMPLATES[tplid] = tpl
if settings: TEMPLATES[tplid].prepare(**settings)
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
else:
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
if not TEMPLATES[tplid]:
abort(500, 'Template (%s) not found' % tpl)
for dictarg in args[1:]: kwargs.update(dictarg)
return TEMPLATES[tplid].render(kwargs)

mako_template = functools.partial(template, template_adapter=MakoTemplate)
cheetah_template = functools.partial(template, template_adapter=CheetahTemplate)
jinja2_template = functools.partial(template, template_adapter=Jinja2Template)

template 封装了所有模板引擎的使用,默认使用 SimpleTemplate,最终会调用 render 方法,这里我们只看 SimpleTemplateSimpleTemplate 继承自 BaseTemplatepreparerender 是一个必须实现的方法,SimpleTemplaterender 会调用 execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def execute(self, _stdout, kwargs):
env = self.defaults.copy()
env.update(kwargs)
env.update({'_stdout': _stdout, '_printlist': _stdout.extend,
'include': functools.partial(self._include, env),
'rebase': functools.partial(self._rebase, env), '_rebase': None,
'_str': self._str, '_escape': self._escape, 'get': env.get,
'setdefault': env.setdefault, 'defined': env.__contains__ })
eval(self.co, env)
if env.get('_rebase'):
subtpl, rargs = env.pop('_rebase')
rargs['base'] = ''.join(_stdout) #copy stdout
del _stdout[:] # clear stdout
return self._include(env, subtpl, **rargs)
return env

execute 将用户传入的参数和辅助函数加入到 env 字典中,通过 eval 执行编译后的模板代码,其中 codeco 都是使用 cached_property 装饰器进行装饰可以缓存的属性,code 调用 StplParser 获得模板中的可解析的 Python 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@cached_property
def code(self):
source = self.source
if not source:
with open(self.filename, 'rb') as f:
source = f.read()
try:
source, encoding = touni(source), 'utf8'
except UnicodeError:
depr('Template encodings other than utf8 are no longer supported.') #0.11
source, encoding = touni(source, 'latin1'), 'latin1'
parser = StplParser(source, encoding=encoding, syntax=self.syntax)
code = parser.translate()
self.encoding = parser.encoding
return code

StplParser 这里不再展开,实际上没那么复杂,它的作用是用正则表达式匹配 Python 代码然后组装在一起返回,co 返回根据 code 编译的结果。

1
2
3
@cached_property
def co(self):
return compile(self.code, self.filename or '<string>', 'exec')

对于模板的使用,还有另一种方法,那就是使用 view 装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def view(tpl_name, **defaults):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if isinstance(result, (dict, DictMixin)):
tplvars = defaults.copy()
tplvars.update(result)
return template(tpl_name, **tplvars)
elif result is None:
return template(tpl_name, defaults)
return result
return wrapper
return decorator

mako_view = functools.partial(view, template_adapter=MakoTemplate)
cheetah_view = functools.partial(view, template_adapter=CheetahTemplate)
jinja2_view = functools.partial(view, template_adapter=Jinja2Template)

对于返回时调用 template 的代码

1
2
def index(name='World'):
return template('hello_template', name=name)

可以改为:

1
2
3
@view('hello_template')
def index(name='World'):
return {'name': name}

总结

Bottle 是一个小而巧的框架,它可以用来进行原型开发或构建比较小的 Web 应用或服务,但缺乏很多高级功能,比如 ORM,表单验证等等,如果需求是开发一个复杂的应用,使用 Django 或 Flask 可能更合适。有意思的是 Flask 的诞生和 Bottle 有很大的关系,如果希望使用 Bottle 的一系列装饰器的语法糖,可以使用 Flask。

从源码的角度来看,阅读 bottle.py 的源码是一件很有价值的事情,可以在 4000 行代码内了解一个 Python web 框架需要做的事情本身就很有趣。需要注意的是,bottle 的代码只引用了 python 标准库,但它是同时支持 python2,python3 的,代码比较啰嗦,对于同时兼容 python2,python3,six 库的实践可能更有参考意义,其次,代码本身很多地方不遵循 PEP8 规范,这对于有代码洁癖的人来说还是挺难受的。

代码解析的部分内容不是很多,只写了一些比较关键的功能实现,虽然 bottle.py 的代码不多,把每一行代码都解释一遍再记录下来也没有必要,不过未来有时间的话,可以继续深入如插件功能,描述符和装饰器的运用等代码片段。

总的来说,bottle.py 的代码还是非常值得阅读,参考。

Reference